package uk.co.q3c.deplan.server.service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.log4j.Logger;

import uk.co.q3c.deplan.client.domain.DomainKind;
import uk.co.q3c.deplan.client.domain.DomainObject;
import uk.co.q3c.deplan.client.domain.dag.DAG;
import uk.co.q3c.deplan.client.domain.dag.Hierarchy;
import uk.co.q3c.deplan.client.domain.dag.SharedStructure;
import uk.co.q3c.deplan.client.domain.dag.SharedStructureChangeLog;
import uk.co.q3c.deplan.client.domain.dag.Vertex;
import uk.co.q3c.deplan.client.domain.resource.PersistenceReferenceException;
import uk.co.q3c.deplan.client.domain.task.BaseTask;
import uk.co.q3c.deplan.client.domain.task.Task;
import uk.co.q3c.deplan.client.service.PlanManager;
import uk.co.q3c.deplan.client.service.TaskService;
import uk.co.q3c.deplan.client.util.Q3DateUtil;
import uk.co.q3c.deplan.server.Server;
import uk.co.q3c.deplan.server.dao.DomainDao;

import com.google.appengine.repackaged.com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.NotFoundException;
import com.googlecode.objectify.Query;

/**
 * 
 *<b>NOTE:</b> If ever there is a server side background task which modifies dependencies or hierarchy, there will need to be a client side
 * and a server side structure, as they could be modified concurrently.
 * 
 * @see ResourceService_BigT
 * @author David Sowerby 7 Aug 2010
 * 
 */
@SuppressWarnings("serial")
public class TaskService_BigT extends ServiceProviderBase implements TaskService {

	private final SharedStructureChangeLog dependenciesChangeLog;

	private final Key<DAG<Key<? extends Task>>> dependenciesKey;
	private Hierarchy<Key<? extends Task>> hierarchy;
	private final SharedStructureChangeLog hierarchyChangeLog;
	private final Key<Hierarchy<? extends Task>> hierarchyKey;
	transient protected final Logger logger = Logger.getLogger(getClass().getName());
	private DAG<Key<? extends Task>> workingDag;

	@SuppressWarnings("unchecked")
	public TaskService_BigT(DomainDao dao) {
		super(dao);
		dependenciesKey = new Key<DAG<Key<? extends Task>>>(null, (Class<? extends DAG<Key<? extends Task>>>) DAG.class, new Long(1));
		hierarchyKey = new Key<Hierarchy<? extends Task>>(null, (Class<? extends Hierarchy<? extends Task>>) Hierarchy.class, new Long(1));
		String sharedstructuremsg = "Unable to continue without access to the dependency graph and hierarchy ";

		try {
			hierarchy = (Hierarchy<Key<? extends Task>>) dao.getObjectFromKey(hierarchyKey);
			workingDag = dao.getObjectFromKey(dependenciesKey);
		} catch (Exception e) {
			logger.fatal(sharedstructuremsg + e.getMessage());
		}
		if (hierarchy == null || (workingDag == null)) {
			logger.fatal(sharedstructuremsg);
		}
		dependenciesChangeLog = new SharedStructureChangeLog();
		hierarchyChangeLog = new SharedStructureChangeLog();
		workingDag.setChangeLog(dependenciesChangeLog);
		hierarchy.setChangeLog(hierarchyChangeLog);
		workingDag.setName("Dependency graph");
		hierarchy.setName("Hierarchy structure");
	}

	@Override
	public int countAbstractTasks() {
		return dao.countUndiscarded(BaseTask.class);
	}

	@Override
	public int countPlanManagers() {
		return dao.countUndiscarded(PlanManager.class);
	}

	@Override
	public int countProjects() {
		Query<BaseTask> query = dao.newQuery(BaseTask.class);
		query.filter("project", true)
				.filter("discarded", false);
		return dao.executeCount(query);

	}

	@Override
	public Task findById(DomainKind domainKind, long id) {
		Class<? extends Task> clazz = Server.taskClassForKind(domainKind);
		Key<Task> key = new Key<Task>(clazz, id);
		return dao.getObjectFromKey(key);
	}

	@Override
	public Task findById(long id) {
		ArrayList<Class<? extends DomainObject>> list = Lists.newArrayList(Server.taskClasses.values());
		return (Task) dao.findFirstFromUndiscarded(list, "keyId", id);
	}

	@Override
	public Task findFirstByName(List<Class<? extends DomainObject>> taskClasses, String name) {
		return (Task) dao.findFirstFromAll(taskClasses, "name", name);
	}

	/**
	 * @see TaskService#getDag()
	 */
	@Override
	public DAG<Key<? extends Task>> getDag() {
		if (dependenciesKey == null) {
			if (logger.isDebugEnabled()) {
				logger.error("Requested DAG with null key ");
			}

			return null;
		}
		try {
			if (workingDag == null) {
				if (logger.isTraceEnabled()) {
					logger.trace("key for DAG in Dao is " + dependenciesKey);
				}
				workingDag = dao.getObjectFromKey(dependenciesKey);
			}
			return workingDag;
		} catch (NotFoundException e) {
			if (logger.isDebugEnabled()) {
				logger.fatal("DAG not found for key " + dependenciesKey.toString());
			}
			return null;
		}

	}

	@Override
	public Hierarchy<Key<? extends Task>> getHierarchy() {
		return hierarchy;
	}

	@Override
	public <T extends Task> Key<T> keyFor(T task) {
		return dao.keyFor(task);
	}

	@Override
	public BaseTask parentOf(Task task) throws PersistenceReferenceException {
		List<Vertex<Key<? extends Task>>> vertices = hierarchy.getPredecessors(task.getKey());
		if (vertices.size() == 0) {
			return null;
		} else {
			Key<? extends Task> key = vertices.get(0)
					.getKey();
			return (BaseTask) dao.getObjectFromKey(key);

		}
	}

	@Override
	public List<Task> predecessorsOf(Task task) throws PersistenceReferenceException {
		List<Vertex<Key<? extends Task>>> predVerts = getDag().getPredecessors(task.getKey());
		List<Key<? extends Task>> predkeys = new ArrayList<Key<? extends Task>>();
		for (Vertex<Key<? extends Task>> v : predVerts) {
			predkeys.add(v.getKey());
		}
		return dao.getListOf(predkeys);
	}

	@SuppressWarnings("unchecked")
	@Override
	public void save(Task task) {
		dao.save(task);
		Key<? extends Task> taskKey;
		try {
			taskKey = task.getKey();
			if (task instanceof Task) {
				getDag().addVertex(taskKey);
			}
			SharedStructure dependencies = dao.saveShared(workingDag, dependenciesKey, dependenciesChangeLog);
			// if no changes save was successful (changes cleared after save)
			if (!dependencies.hasChanges()) {
				workingDag = (DAG<Key<? extends Task>>) dependencies;
			}

			getHierarchy().addVertex(taskKey);
			SharedStructure newHierarchy = dao.saveShared(hierarchy, hierarchyKey, hierarchyChangeLog);
			if (!newHierarchy.hasChanges()) {
				hierarchy = (Hierarchy<Key<? extends Task>>) newHierarchy;
			}
		} catch (PersistenceReferenceException e) {
			// will never happen - just saved it
		}

	}

	@Override
	public void setHierarchy(Hierarchy<Key<? extends Task>> hierarchy) {
		this.hierarchy = hierarchy;
	}

	@Override
	public int subTaskCountFor(BaseTask summaryTask) {
		if (summaryTask == null) {
			return 0;
		}

		try {
			List<Vertex<Key<? extends Task>>> vertices = hierarchy.getSuccessors(summaryTask.getKey());
			return vertices.size();
		} catch (PersistenceReferenceException e) {
			// swallow - cannot have children without having a key
			return 0;
		}

	}

	@Override
	public List<Key<? extends Task>> subTasksKeysFor(BaseTask summaryTask) throws PersistenceReferenceException {
		List<Vertex<Key<? extends Task>>> vertices = hierarchy.getSuccessors(summaryTask.getKey());
		List<Key<? extends Task>> keys = new ArrayList<Key<? extends Task>>();
		for (Vertex<Key<? extends Task>> v : vertices) {
			keys.add(v.getKey());
		}
		return keys;
	}

	@Override
	public List<? extends Task> subTasksOf(BaseTask summaryTask) throws PersistenceReferenceException {
		List<Key<? extends Task>> keys = subTasksKeysFor(summaryTask);
		return dao.getListOf(keys);
	}

	@Override
	public List<Task> successorsOf(Task task) throws PersistenceReferenceException {
		List<Vertex<Key<? extends Task>>> succVerts = getDag().getSuccessors(task.getKey());
		List<Key<? extends Task>> succkeys = new ArrayList<Key<? extends Task>>();
		for (Vertex<Key<? extends Task>> v : succVerts) {
			succkeys.add(v.getKey());
		}
		return dao.getListOf(succkeys);
	}

	@Override
	public int childCount(BaseTask at1) throws PersistenceReferenceException {
		return getHierarchy().getVertex(at1.getKey())
				.childCount();
	}

	/**
	 * Rolls up values from sub tasks to tasks
	 */
	@Override
	public void rollup(Task task) throws PersistenceReferenceException {
		List<? extends Task> subTasks = subTasksOf((BaseTask) task);

		// rollup
		if (subTasks.size() > 0) {
			int wr = 0;
			int wd = 0;
			int wa = 0;
			Date actualStart = null;
			Date actualFinish = null;
			Date plannedFinish = null;
			Date startRemaining = null;

			for (Task subTask : subTasks) {
				// has to be done first, as this has to go to the bottom of the structure and roll back up
				rollup(subTask);
				wr += subTask.getWorkRemaining();
				wd += subTask.getWorkDone();
				wa += subTask.getWorkAllocated();
				actualStart = Q3DateUtil.earliestOf(actualStart, subTask.getActualStart());
				startRemaining = Q3DateUtil.earliestOf(startRemaining, subTask.getStartRemaining());
				actualFinish = Q3DateUtil.latestOf(actualFinish, subTask.getActualFinish());
				plannedFinish = Q3DateUtil.latestOf(plannedFinish, subTask.getPlannedFinish());
			}
			task.setWorkRemaining(wr);
			task.setWorkDone(wd);
			task.setWorkAllocated(wa);
			task.setActualStart(actualStart);
			task.setStartRemaining(startRemaining);
			task.setActualFinish(actualFinish);
			task.setPlannedFinish(plannedFinish);
		}
	}

	/**
	 * Pushes down values from parent to child in task hierarchy - at the moment that is only applicable to priority
	 * 
	 * @param task
	 * @throws PersistenceReferenceException
	 */
	@Override
	public void pushDown(Task task) throws PersistenceReferenceException {
		List<? extends Task> subTasks = subTasksOf((BaseTask) task);
		// pushDown
		if (subTasks.size() > 0) {
			for (Task subTask : subTasks) {
				subTask.setParentPriority(task.getPriority());
				pushDown(subTask);
			}
		}
	}

	@Override
	public boolean taskIsAtomic(Task task) throws PersistenceReferenceException {
		return getHierarchy().isAtomic(task.getKey());
	}

}
